Перейти к основному содержимому

3.06. MongoDB

Разработчику Аналитику Тестировщику
Архитектору Инженеру

MongoDB

MongoDB — это распределённая, документо-ориентированная система управления базами данных с открытым исходным кодом, изначально разработанная для поддержки высоконагруженных, динамически изменяющихся и масштабируемых приложений. В отличие от реляционных СУБД, где данные организуются в строгие табличные структуры с фиксированными схемами, MongoDB основана на концепции документа — автономной, самодостаточной единицы данных, хранящейся в бинарном представлении JSON (BSON). Такой подход позволяет гибко работать со сложными, вложенными и неоднородными структурами без необходимости предварительного проектирования нормализованной схемы.

Концептуальные основы

В реляционных СУБД ключевой строительный блок — строка таблицы: запись, состоящая из фиксированного набора полей, определённых заранее в схеме таблицы. Все строки одной таблицы имеют одинаковую структуру. В MongoDB аналогом строки является документ, а таблицы — коллекция. Однако принципиальное отличие в том, что коллекция не накладывает ограничений на структуру хранящихся в ней документов. Один документ может содержать поля name, age, hobbies; другой — лишь title и timestamp; третий — вложенный объект с произвольной глубиной вложенности. Такая гибкость называется схемой по записи (schema-on-write в реляционных системах против schema-on-read в документных), и она особенно ценна в условиях быстро меняющихся требований к данным, характерных для современных приложений.

Каждый документ в MongoDB обязан содержать поле _id, играющее роль первичного ключа. Значением _id может быть любое допустимое BSON-значение, однако по умолчанию, если при вставке документа поле _id не указано, драйвер MongoDB генерирует его как ObjectId — 12-байтовое значение, состоящее из временной метки создания документа, идентификатора машины, идентификатора процесса и счётчика. Такая структура позволяет однозначно идентифицировать документы в распределённой среде без централизованного координатора и обеспечивает естественную сортировку по времени создания.

База данных в MongoDB — это логический контейнер, объединяющий одну или несколько коллекций, а также другие объекты: индексы, представления, пользовательские функции. База данных является основной единицей административного управления: на неё накладываются права доступа, политики резервного копирования, определяются параметры репликации. Одна инсталляция MongoDB (точнее, один сервер mongod или кластер mongos) может содержать множество баз данных, каждая со своей собственной логикой и набором коллекций.

Таким образом, иерархия организации данных в MongoDB выглядит следующим образом:

  • Сервер/Кластер MongoDB
    • База данных (Database)
      • Коллекция (Collection)
        • Документ (Document)
          • Поля (Fields) с BSON-значениями.

Эта иерархия напрямую отражается в синтаксисе оболочки MongoDB: db.collectionName.find(...) — где db — текущая база данных, а collectionName — её коллекция.

BSON

MongoDB использует BSON (Binary JSON) — расширение JSON, оптимизированное для эффективного хранения и быстрого обхода. Хотя BSON часто называют «бинарным JSON», это самостоятельный двоичный формат с собственными типами данных и правилами сериализации.

Основные отличия BSON от обычного JSON:

  1. Типизация. В JSON существует ограниченный набор типов: строки, числа (не делятся на int/float), булевы значения, null, объекты и массивы. BSON, напротив, поддерживает более двадцати явных типов, включая:

    • 32-битные и 64-битные целые (int32, int64);
    • 32-битные и 64-битные числа с плавающей точкой (double, decimal128);
    • логические значения;
    • null;
    • строки;
    • двоичные данные (BinData);
    • объекты (Object);
    • массивы;
    • временные метки (Date);
    • регулярные выражения;
    • ObjectId;
    • минимум и максимум (MinKey, MaxKey);
    • JavaScript-код (Code) — устаревший, но поддерживаемый тип.

    Явная типизация позволяет избежать неоднозначностей при десериализации и обеспечивает корректную сортировку и сравнение (например, целое число 100 будет корректно сравниваться как число, а не как строка "100").

  2. Эффективность. BSON хранится в бинарной форме, что позволяет избежать накладных расходов на парсинг текстового JSON при каждом чтении или записи. Данные могут быть частично десериализованы без полной загрузки документа в память — например, драйвер может быстро извлечь значение _id, не обрабатывая остальное содержимое документа. Это критически важно для производительности в высоконагруженных системах.

  3. Поддержка двоичных данных. BSON включает специальный тип BinData, что делает возможным хранение изображений, аудио, видео или сериализованных объектов непосредственно в документах (хотя для очень больших бинарных объектов рекомендуется использовать GridFS — расширение MongoDB для хранения файлов).

  4. Самоописываемость. Каждое поле в BSON-документе предваряется своим типом и длиной, а весь документ начинается с 4-байтового заголовка, содержащего общую длину документа. Это позволяет быстро пропускать документы при сканировании и эффективно читать их в произвольном порядке.

Поскольку BSON является строгим надмножеством JSON, любой корректный JSON-объект может быть преобразован в BSON и обратно без потерь информации (при условии, что числа не теряют точности — например, целые вне диапазона int64 могут быть преобразованы в double). Обратное неверно: не каждый BSON-документ может быть корректно представлен в виде JSON без дополнительных аннотаций (например, Date или ObjectId в JSON требуют специального оборачивания в объект { "$date": "..." }).

Экосистема MongoDB

MongoDB — это не только ядро СУБД (mongod). Это развитая экосистема, включающая инструменты для разработки, администрирования, мониторинга и развёртывания.

Ядро и сервисы

  • mongod — основной процесс сервера базы данных. Он отвечает за хранение данных, обработку запросов, управление индексами, транзакциями, репликацией и шардированием. Может работать как в standalone-режиме, так и в составе реплика-сета или шардированного кластера.
  • mongos — маршрутизатор запросов для шардированных кластеров. Он принимает запросы от клиентов, определяет, к каким шардам они относятся, распределяет их и агрегирует результаты.
  • mongosh — современная интерактивная оболочка MongoDB, заменяющая устаревший mongo. Поддерживает синтаксис JavaScript, автодополнение, подсветку синтаксиса, историю команд и расширения. Является основным инструментом для интерактивной диагностики и администрирования.
  • MongoDB Atlas — облачная fully-managed платформа MongoDB, предоставляемая компанией MongoDB Inc. Управляет инфраструктурой, репликацией, шардированием, резервным копированием, обновлениями, мониторингом и безопасностью. Поддерживает развёртывание в AWS, Azure, Google Cloud.

Административные и разработческие инструменты

  • MongoDB Compass — официальный графический интерфейс для MongoDB, позволяющий визуально исследовать данные, строить запросы без знания синтаксиса, анализировать производительность запросов, создавать и управлять индексами, настраивать схемы и валидаторы. Особенно полезен для анализа распределения значений полей и выявления «холодных» или «горячих» участков данных.
  • MongoDB Shell Extensions — плагины для mongosh, расширяющие его функциональность: поддержка TypeScript, улучшенное форматирование, интеграция с Atlas и т.д.
  • MongoDB Database Tools — набор утилит командной строки: mongodump/mongorestore (резервное копирование и восстановление), mongoexport/mongoimport (экспорт/импорт в JSON или CSV), bsondump (просмотр BSON в человекочитаемом виде), mongofiles (управление GridFS).

Драйверы и ODM/ORM

MongoDB предоставляет официальные драйверы для всех основных языков: C, C++, C#, Go, Java, Node.js, Perl, PHP, Python, Ruby, Rust, Scala. Драйверы реализуют протокол обмена с сервером, обработку соединений, сериализацию BSON и базовую логику управления транзакциями.

Помимо драйверов, существуют высокоуровневые абстракции:

  • Mongoose (Node.js) — popular ODM (Object Data Mapper), добавляющий строгую схему, валидацию, middleware и методы-хелперы.
  • PyMongo + MongoEngine (Python) — PyMongo — низкоуровневый драйвер, MongoEngine — ODM поверх него.
  • Spring Data MongoDB (Java) — часть экосистемы Spring, предоставляет шаблоны репозиториев, аннотации для сопоставления объектов и интеграцию с транзакционным менеджером Spring.

Дополнительные компоненты

  • MongoDB Realm — платформа для создания full-stack приложений с синхронизацией данных между клиентом и сервером в реальном времени (ранее — Stitch).
  • MongoDB Charts — инструмент визуализации данных, позволяющий строить графики и дашборды по данным MongoDB без написания кода.
  • MongoDB Kafka Connector — интеграция с Apache Kafka для потоковой передачи изменений в MongoDB (Change Streams → Kafka).

Эта экосистема позволяет охватить весь жизненный цикл данных: от проектирования и разработки приложения до развёртывания, мониторинга и аналитики.


CRUD-операции

Операции создания, чтения, обновления и удаления в MongoDB реализованы через методы, работающие на уровне коллекций. Все они принимают BSON-объекты в качестве параметров и возвращают структурированные результаты (включая метаданные: количество затронутых документов, статус записи, время выполнения при профилировании и т.п.).

1. Вставка: insertOne и insertMany

Метод insertOne(document, options?) вставляет один документ в коллекцию. Если в документе отсутствует поле _id, драйвер автоматически генерирует для него ObjectId. Если _id указан явно и уже существует в коллекции, операция завершается ошибкой E11000 duplicate key error, поскольку _id всегда имеет уникальный индекс.

Метод insertMany(documentsArray, options?) позволяет вставить массив документов за одно обращение к серверу. Это существенно эффективнее, чем N вызовов insertOne, особенно при работе по сети. По умолчанию вставка выполняется последовательно и атомарно для каждого документа, но не для всего массива в целом: если при вставке k-го документа возникнет ошибка (например, дубликат _id), операция прекращается, и последующие документы не вставляются. Это поведение контролируется флагом ordered:

  • ordered: true (по умолчанию) — остановка при первой ошибке;
  • ordered: false — сервер продолжает обработку оставшихся документов, несмотря на ошибки, и возвращает агрегированную информацию обо всех сбоях.

Оба метода принимают опцию writeConcern, определяющую уровень подтверждения записи. Например, { w: "majority", wtimeout: 5000 } означает: дождаться подтверждения от большинства узлов реплика-сета, но не более 5 секунд; при таймауте операция завершится с ошибкой, хотя запись могла быть выполнена частично.

2. Чтение: find и findOne

Метод findOne(filter?, projection?, options?) возвращает один документ, соответствующий фильтру (первый в порядке сканирования), или null, если совпадений нет. Он не возвращает курсор — результат сразу материализуется. Этот метод удобен для поиска по уникальному полю (включая _id).

Метод find(filter?, projection?, options?) возвращает курсор — объект, позволяющий лениво итерироваться по результатам запроса. Курсор извлекает данные пачками (batch size регулируется на стороне сервера и клиента). Это критически важно при работе с большими выборками: приложение получает данные по мере необходимости, не расходуя избыточную память.

Важно понимать, что find() без фильтра возвращает все документы коллекции. Это может привести к значительной нагрузке на сеть и клиент, если коллекция содержит миллионы записей. Всегда применяйте ограничения (limit, skip, sort) или фильтры в production-сценариях.

3. Обновление: updateOne, updateMany, replaceOne

MongoDB различает модификационные и заменяющие операции.

  • updateOne(filter, update, options?) и updateMany(filter, update, options?) применяют операторы обновления ($set, $inc, $push и др.) к существующим документам. Обновляется только указанные поля; остальная структура документа сохраняется. Поведение по умолчанию: если фильтр не находит ни одного документа — операция «тихо» завершается без изменений.

  • replaceOne(filter, replacement, options?) полностью заменяет найденный документ новым объектом (кроме _id, который сохраняется, если не указан иначе). Все поля оригинала, отсутствующие в replacement, теряются.

Ключевая опция — upsert: true. При её включении, если фильтр не находит совпадений, MongoDB вставляет новый документ. При этом:

  • Если в update используется $setOnInsert, его поля применяются только при вставке;
  • Поле _id при вставке генерируется автоматически, если не указано явно в фильтре или в replacement/update.

Другие важные опции:

  • arrayFilters — позволяет адресовать конкретные элементы массива по условию, например:
    updateMany({}, { $set: { "grades.$[g]": 100 } }, { arrayFilters: [ { g: { $gte: 90 } } ] }).
  • hint — явное указание индекса для выполнения обновления.

4. Удаление: deleteOne, deleteMany

Методы deleteOne(filter, options?) и deleteMany(filter, options?) удаляют документы, соответствующие фильтру. Удаление не возвращает сами документы — только счётчик (deletedCount). Для получения удалённого документа следует использовать findOneAndDelete(filter, options?), который атомарно находит, возвращает и удаляет один документ.

Удаление всей коллекции через deleteMany({}) не удаляет метаданные коллекции: индексы, валидаторы, параметры TTL — сохраняются. Физическое освобождение дискового пространства происходит асинхронно (в зависимости от storage engine: WiredTiger выполняет фоновую компактизацию). Чтобы полностью уничтожить коллекцию со всей структурой — используется db.collection.drop().


Построение запросов

Запросы в MongoDB строятся с помощью BSON-объектов, содержащих операторы запроса — специальные ключи, начинающиеся с символа $.

1. Операторы сравнения

  • $eq, $ne — равно / не равно (явно указываются, если требуется семантика «точного совпадения», включая тип);
  • $gt, $gte, $lt, $lte — строгое и нестрогое сравнение;
  • $in, $nin — проверка вхождения значения в массив (или отсутствия в нём).
    Пример: { status: { $in: ["active", "pending"] } }.

Важно: сравнение значений разных типов подчиняется правилам BSON-сортировки: null < числа < строки < объекты < массивы < бинарные данные < ObjectId < булевы < даты < регулярные выражения < MinKey < MaxKey. Это влияет как на сортировку, так и на результаты $gt/$lt.

2. Логические операторы

  • $and — конъюнкция (по умолчанию используется при перечислении полей: {a: 1, b: 2} эквивалентно { $and: [{a: 1}, {b: 2}] });
  • $or — дизъюнкция;
  • $not — логическое отрицание (применяется к одному условию: { name: { $not: { $eq: "admin" } } });
  • $nor — отрицание дизъюнкции ({ $nor: [{a: 1}, {b: 1}] } ⇔ ни a=1, ни b=1).

Сложные запросы строятся рекурсивно. Например, выборка документов, где (age < 18 или age > 65) и status ≠ "blocked":

{
$and: [
{ $or: [ { age: { $lt: 18 } }, { age: { $gt: 65 } } ] },
{ status: { $ne: "blocked" } }
]
}

3. Операторы элементов и массивов

  • $exists: true|false — проверка наличия поля;
  • $type: "<typeName>" | <typeNumber> — проверка типа значения (например, "string" или 2);
  • $all — все указанные элементы должны присутствовать в массиве (независимо от порядка);
  • $size — точное совпадение размера массива (не поддерживает индексацию — требует сканирования);
  • $elemMatch — условие применяется к одному и тому же элементу вложенного массива объектов.
    Пример: найти студентов, у которых есть оценка ≥85 по предмету "math":
    { grades: { $elemMatch: { subject: "math", score: { $gte: 85 } } } }.

Курсоры

Как уже отмечалось, find() возвращает курсор — объект, поддерживающий цепочку методов для последовательной обработки результата.

  • .sort(sortSpec) — задаёт порядок сортировки: { field: 1 } — по возрастанию, { field: -1 } — по убыванию. Можно сортировать по нескольким полям. Важно: сортировка без подходящего индекса может потребовать сортировки в памяти (ограничение по умолчанию — 32 МБ; при превышении — ошибка, если не указан allowDiskUse: true).

  • .limit(n) — ограничивает количество возвращаемых документов. Эффективно используется вместе с sort, чтобы получить топ-N результатов.

  • .skip(n) — пропускает первые n документов. Часто применяется для пагинации, но неэффективно при больших смещениях (skip(10000).limit(10) требует сканирования 10 010 документов). Для глубокой пагинации рекомендуется курсорная пагинация на основе последнего значения сортируемого поля.

  • .projection({ field: 1 | 0 }) — управляет возвращаемыми полями: 1 — включить, 0 — исключить (нельзя смешивать в одном запросе, кроме как для _id). Исключение тяжёлых полей (например, payload, logs) значительно снижает объём передаваемых данных.

  • .hint(indexName) — явное указание индекса, что полезно при отладке или неоднозначных планах.

  • .explain("executionStats") — возвращает детали выполнения запроса: использованный индекс, число просканированных и возвращённых документов (totalDocsExamined vs nReturned), время выполнения. Ключевой инструмент для оптимизации.

Курсоры имеют ограниченное время жизни на сервере (обычно 10 минут неактивности). При превышении — ошибка cursor not found. В прикладном коде следует избегать длительных пауз между hasNext() и next().


Индексы

Индекс в MongoDB — это отдельная структура данных (B-дерево или его вариации), которая хранит отсортированные значения одного или нескольких полей и ссылки на соответствующие документы. Индексы позволяют избежать полного сканирования коллекции (COLLSCAN) и переходить к нужным документам напрямую (IXSCAN).

Основные типы индексов

  • Однопольный индекс ({ field: 1 }) — самый простой и часто используемый.
  • Составной индекс ({ a: 1, b: -1 }) — эффективен для запросов, фильтрующих по a, или по a и b, или сортирующих по (a, b). Порядок полей критичен: префикс индекса должен совпадать с фильтром.
  • Уникальный индекс ({ email: 1 }, { unique: true }) — гарантирует отсутствие дубликатов по указанному полю(ям). Ошибка при вставке/обновлении вызывает исключение E11000.
  • Частичный индекс ({ status: "active" }, { partialFilterExpression: { status: "active" } }) — индексирует только документы, удовлетворяющие условию. Экономит место и ускоряет обновления для редко запрашиваемых подмножеств.
  • TTL-индекс ({ createdAt: 1 }, { expireAfterSeconds: 86400 }) — автоматически удаляет документы, возраст которых превышает указанное число секунд. Используется для логов, сессий, временных задач.
  • Текстовый индекс ({ title: "text", body: "text" }) — поддерживает полнотекстовый поиск с ранжированием по релевантности ({ $meta: "textScore" }).
  • Геопространственные индексы (2dsphere, 2d) — для запросов вида «найти точки в радиусе», «ближайшие объекты».

Принципы эффективного индексирования

  1. Покрывающие индексы — когда все запрашиваемые поля присутствуют в индексе. MongoDB может выполнить запрос, не обращаясь к самим документам (FETCH stage отсутствует).
  2. Выборочность — индекс эффективен, если отсекает значительную часть данных. Индекс по полю gender (2 значения) почти бесполезен при фильтре gender: "M", если половина коллекции — мужчины.
  3. Накладные расходы — каждый индекс замедляет вставку, обновление и удаление (т.к. нужно обновлять и структуру индекса). Оптимальное число индексов — 3–7 на коллекцию в большинстве сценариев.
  4. Комбинирование фильтрации, сортировки и проекции — индекс может обслуживать все три этапа одновременно, если его структура соответствует порядку: равенство → сортировка → диапазон (правило ESR: Equality, Sort, Range).

Анализ эффективности индексов проводится через explain(), а также команды db.collection.totalIndexSize(), db.collection.stats().


Агрегация данных

Агрегация в MongoDB — это механизм преобразования, фильтрации, группировки и вычисления данных непосредственно на стороне сервера. В отличие от клиентской обработки результата find(), агрегация минимизирует объём передаваемых данных и использует оптимизированные внутренние алгоритмы, что критично для производительности при работе с миллионами записей.

Основная абстракция — конвейер (pipeline): последовательность этапов (stages), через которые проходят документы. Каждый этап принимает поток документов, применяет к ним преобразование и передаёт результат следующему этапу. Конвейер задаётся массивом BSON-объектов, где каждый объект соответствует одному этапу и имеет ровно одно поле с ключом, начинающимся с $.

Базовые этапы конвейера

  • $match — фильтрация документов по условию, аналогичному параметру find().

    { $match: { status: "active", createdAt: { $gte: ISODate("2025-01-01") } } }

    Применяется как можно раньше в конвейере: уменьшает объём данных на последующих этапах и позволяет использовать индексы.

  • $project — переопределение структуры документа: включение, исключение, вычисление новых полей.

    { $project: {
    name: 1,
    emailDomain: { $split: ["$email", "@"] },
    isAdult: { $gte: ["$age", 18] }
    } }

    Поддерживает арифметические, строковые, логические, датовые и условные операторы ($add, $substr, $cond, $dateToString, $size, $type и др.). Можно использовать $project для «разворачивания» вложенных объектов ($mergeObjects) или упрощения структуры перед $group.

  • $group — агрегация документов по указанному ключу (или _id: null для глобальной группировки) с применением аккумуляторных операторов.

    { $group: {
    _id: "$department",
    avgSalary: { $avg: "$salary" },
    totalEmployees: { $sum: 1 },
    maxSalary: { $max: "$salary" },
    employees: { $push: "$name" } // собирает все имена в массив
    } }

    Поддерживаемые аккумуляторы: $sum, $avg, $min, $max, $first, $last, $push, $addToSet, $stdDevPop, $stdDevSamp.

  • $sort — упорядочивание документов. Работает идентично .sort() у курсора, но в рамках конвейера может потребовать allowDiskUse: true, если объём данных превышает лимит памяти (100 МБ по умолчанию).
    Часто используется перед $limit для выборки топ-N.

  • $skip и $limit — пагинация результатов. Как и при работе с курсорами, skip неэффективен при больших смещениях; для глубокой пагинации предпочтителен паттерн «range-based pagination» с $match по последнему известному значению сортируемого поля.

Продвинутые этапы

  • $unwind — развёртывание массива: на каждый элемент массива создаётся отдельный документ. Удобен для анализа вложенных коллекций.

    { $unwind: "$tags" } // { _id: 1, tags: ["A", "B"] } → два документа: { _id: 1, tags: "A" }, { _id: 1, tags: "B" }

    Опция preserveNullAndEmptyArrays: true сохраняет документы, где поле отсутствует, null или пустой массив.

  • $lookup — выполнение операции, аналогичной left outer join в SQL. Позволяет объединять данные из разных коллекций без денормализации.

    { $lookup: {
    from: "orders",
    localField: "_id",
    foreignField: "customerId",
    as: "orders"
    } }

    Используется с осторожностью: при отсутствии индекса по foreignField может привести к полному сканированию целевой коллекции на каждый документ.

  • $facet — одновременный запуск нескольких независимых подконвейеров и объединение их результатов в один документ. Применяется для построения сложных отчётов: например, агрегация + распределение по интервалам + топ-10 — всё в одном запросе.

    { $facet: {
    summary: [ { $group: { _id: null, total: { $sum: 1 } } } ],
    byStatus: [ { $group: { _id: "$status", count: { $sum: 1 } } } ],
    topAuthors: [ { $sort: { views: -1 } }, { $limit: 5 } ]
    } }
  • $bucket и $bucketAuto — автоматическое распределение документов по диапазонам (bucketing). Полезно для построения гистограмм.

    { $bucket: {
    groupBy: "$price",
    boundaries: [0, 100, 500, 1000, Infinity],
    default: "other",
    output: { count: { $sum: 1 } }
    } }
  • $setWindowFields (начиная с MongoDB 5.0) — оконные функции: вычисление скользящих средних, рангов, накопительных сумм без группировки.

    { $setWindowFields: {
    partitionBy: "$department",
    sortBy: { salary: -1 },
    output: {
    rank: { $rank: {} },
    runningTotal: { $sum: "$salary", window: { documents: ["unbounded", "current"] } }
    }
    } }

Конвейер оптимизируется автоматически: MongoDB переставляет этапы, чтобы максимально использовать индексы (например, sort + limit может быть заменён на «top-k» операцию с индексом), убирает избыточные project, объединяет последовательные $match.

Для отладки используется explain("executionStats") на агрегации — возвращает план выполнения, объём данных на каждом этапе, время обработки и использование индексов.


Транзакции

До версии 4.0 MongoDB поддерживала только одно-документные атомарные операции. С выходом 4.0 появилась поддержка много-документных транзакций с гарантиями ACID (Atomicity, Consistency, Isolation, Durability), сначала для реплика-сетов, а с 4.2 — и для шардированных кластеров.

Транзакции реализованы на уровне сессии — объекта, представляющего клиентский контекст. Сессия создаётся явно (client.startSession()) или неявно (при использовании withTransaction() в драйверах). Все операции в рамках транзакции выполняются в одной сессии.

Пример (синтаксис mongosh, псевдокод):

const session = db.getMongo().startSession();
session.startTransaction();
try {
db.accounts.updateOne({ _id: "A" }, { $inc: { balance: -100 } }, { session });
db.accounts.updateOne({ _id: "B" }, { $inc: { balance: +100 } }, { session });
session.commitTransaction();
} catch (error) {
session.abortTransaction();
throw error;
} finally {
session.endSession();
}

Ключевые ограничения и особенности

  • Время жизни: транзакция автоматически прерывается, если не завершена в течение 60 секунд (по умолчанию; настраивается через transactionLifetimeLimitSeconds). Продление невозможно.
  • Размер лога (oplog): суммарный объём изменений в транзакции не должен превышать 16 МБ (размер одного документа в oplog). При превышении — ошибка.
  • Совместимость с операциями: не поддерживаются операции, изменяющие схему (createCollection, drop, createIndex), а также distinct, mapReduce, geoNear, text search внутри транзакции.
  • Уровни изоляции: MongoDB использует snapshot isolation — транзакция работает с согласованным снимком данных на момент её начала. Читает не видят неподтверждённых записей других транзакций; писатели не блокируют читателей (MVCC на базе WiredTiger).
  • Производительность: транзакции накладывают издержки — логирование в transaction table, управление снапшотами. Их следует использовать только там, где требуется строгая согласованность (денежные переводы, инвентаризация), а не «на всякий случай».

Для большинства сценариев (например, обновление профиля пользователя) достаточно одно-документной атомарности (updateOne с $set, $inc, $push и т.д.), так как документ сам по себе — естественная граница согласованности.


Практическое задание

Цель: ознакомиться с графическим интерфейсом, визуально исследовать структуру данных, выполнить CRUD-операции и проанализировать производительность запросов.

Шаг 1. Установка

  1. Перейдите на официальную страницу загрузок: https://www.mongodb.com/try/download/compass
  2. Выберите версию, соответствующую вашей ОС (Windows, macOS, Linux).
  3. Запустите установщик и следуйте инструкциям (на Linux — распакуйте архив и запустите бинарный файл).

Примечание: Compass не требует предварительной установки MongoDB Server, но для работы ему нужен запущенный mongod (локальный или удалённый, например, Atlas).

Шаг 2. Подключение

  1. Откройте Compass.
  2. В поле Connection String введите:
    • Для локального сервера по умолчанию: mongodb://localhost:27017
    • Для MongoDB Atlas: строка подключения из панели Atlas (вида mongodb+srv://<user>:<password>@cluster0.xxx.mongodb.net/).
  3. Нажмите Connect.

Шаг 3. Исследование базы данных

  1. После подключения вы увидите список баз данных слева.
  2. Выберите базу sample_mflix (если используете Atlas) или создайте свою (CREATE DATABASE).
  3. Перейдите в коллекцию movies.
  4. Вкладка Documents показывает первые 20 документов. Используйте фильтр вверху (Filter) для поиска:
    { "year": { "$gte": 2000 }, "imdb.rating": { "$gte": 8.5 } }
  5. Нажмите Explain Plan, чтобы увидеть, какой индекс использован, сколько документов просканировано.

Шаг 4. Создание и обновление данных

  1. Нажмите Insert DocumentInsert Document.
  2. Вставьте JSON:
    {
    "title": "Практическое задание",
    "year": 2025,
    "genres": ["учеба", "IT"],
    "plot": "Первая запись через Compass"
    }
  3. Нажмите Insert.
  4. Найдите этот документ по title, откройте его и нажмите Edit Document. Добавьте поле completed: true. Сохраните.

Шаг 5. Агрегация через Compass

  1. Перейдите во вкладку Aggregations.
  2. Нажмите Add Stage → выберите $match:
    { "year": { "$gte": 2000 } }
  3. Добавьте $group:
    {
    "_id": "$genres",
    "count": { "$sum": 1 }
    }
  4. Нажмите Run, чтобы увидеть распределение фильмов по жанрам.
  5. Добавьте $sort: { "count": -1 } — результат отсортируется по убыванию.

Шаг 6. Анализ и индексы

  1. Перейдите во вкладку Indexes.
  2. Нажмите Create Index.
  3. Укажите поле year: 1, поставьте галочку Unique (снимите, если значения не уникальны), нажмите Create Index.
  4. Вернитесь в Documents, выполните прежний фильтр — в Explain Plan теперь должен отображаться IXSCAN, а не COLLSCAN.

Это задание даёт практическое понимание связи между структурой данных, запросами, индексами и интерфейсом.


Сравнение с реляционными СУБД

MongoDB и классические реляционные СУБД (PostgreSQL, MySQL, SQL Server) решают схожие задачи — хранение, обработка и обеспечение целостности данных, — но делают это с разными акцентами. Отсутствие JOIN’ов, отсутствие строгой схемы и отсутствие табличных нормальных форм в MongoDB — осознанные архитектурные решения, оптимизированные под определённые рабочие нагрузки.

В реляционных системах данные нормализуются для устранения дублирования и обеспечения целостности. Это требует множественных JOIN’ов при чтении, но упрощает обновление: изменение в одном месте распространяется автоматически. В MongoDB предпочтение отдаётся денормализации и вложению — ради уменьшения числа чтений и повышения скорости ответа. Это увеличивает объём хранимых данных и усложняет обновление, но критически важно для высоконагруженных веб- и мобильных приложений, где время отклика — ключевой метрик.

Выбор между моделями следует делать по следующим критериям:

  • Изменчивость схемы: если структура данных постоянно эволюционирует в процессе разработки или эксплуатации — документная модель снижает издержки на миграции.
  • Сложность связей: если домен характеризуется глубокой иерархией (например, заказ → позиции → детали → свойства), вложенные документы отражают её естественно. Если связи плоские и равноправные (например, «пациент — врач — приём — диагноз»), реляционная модель может быть проще.
  • Требования к согласованности: если бизнес-логика требует строгих транзакций между множеством сущностей (банковские переводы, бухгалтерия), реляционные СУБД исторически сильнее. Однако с поддержкой много-документных транзакций MongoDB закрыла этот разрыв для большинства прикладных сценариев.
  • Масштабируемость «по горизонтали»: MongoDB изначально проектировалась для шардирования. Добавление новых узлов и перераспределение данных — штатная операция. В реляционных системах горизонтальное масштабирование требует значительных усилий (Citus, Vitess, шардирование на уровне приложения).

На практике многие современные системы используют гибридный подход: основной поток событий и операционные данные — в MongoDB, а аналитика, отчётность и регуляторные отчёты — в реляционных или OLAP-хранилищах (через CDC, Change Streams, ETL).


Типовые сценарии применения MongoDB

Социальные сети и контент-платформы

Посты, комментарии, лайки, профили пользователей — естественно моделируются как вложенные или связанные документы. Например, пост может содержать массив комментариев, каждый из которых — объект с автором, временем, текстом и, возможно, вложенными ответами. Обновление счётчиков (число лайков) выполняется атомарно через $inc. Для поиска по тегам или тексту используется текстовый индекс или интеграция с Atlas Search.

Каталоги товаров и электронная коммерция

Продукты часто имеют неоднородные атрибуты: у ноутбука — процессор, ОЗУ, видеокарта; у книги — автор, ISBN, жанр. В MongoDB каждый товар может храниться как единый документ с полем specifications, содержащим произвольный набор пар «ключ-значение». Фильтрация по цене, рейтингу, наличию — через индексы. Рекомендации строятся на основе агрегации истории просмотров и покупок.

IoT и временные ряды

Сенсоры генерируют потоки данных с отметкой времени. MongoDB поддерживает коллекции временных рядов (начиная с 5.0) — оптимизированный тип коллекции, где документы группируются по meta (идентификатор устройства) и time, обеспечивая эффективное хранение и запросы по диапазонам времени. Агрегация позволяет строить скользящие средние, детектировать аномалии ($setWindowFields, $stdDevSamp).

Логирование и аналитика событий

Каждое событие (запрос API, клик, ошибка) записывается как документ. Благодаря гибкости схемы, события разных типов могут храниться в одной коллекции с разным набором полей. TTL-индексы автоматически удаляют старые записи. Агрегация позволяет строить dashboards в реальном времени: DAU/MAU, funnel-анализ, топ-10 медленных запросов.

Генеративный ИИ и векторный поиск

MongoDB Atlas предоставляет векторные индексы (на основе HNSW), позволяя хранить и искать эмбеддинги (векторные представления текста, изображений) в той же коллекции, что и операционные данные. Это устраняет необходимость синхронизации между operational и vector-базами. Семантический поиск, рекомендации, RAG (Retrieval-Augmented Generation) реализуются через этап $vectorSearch в агрегационном конвейере — без выгрузки данных во внешние сервисы.


Проектирование схемы

MongoDB не навязывает единственный способ моделирования связей. Разработчик выбирает между:

  • Вложением (embedding) — дочерние документы хранятся внутри родительского.
    Преимущества: атомарная запись, одиночное чтение, естественное отражение иерархии.
    Ограничения: размер документа ≤ 16 МБ; обновление вложенных элементов требует $ или $[]; невозможно индексировать отдельные элементы массива без $**.

  • Ссылками (referencing) — хранение _id связанного документа как поля.
    Преимущества: отсутствие ограничений по размеру; независимое управление жизненным циклом; поддержка циклических связей.
    Недостатки: требуется несколько запросов или $lookup для извлечения связанных данных; отсутствие атомарности между коллекциями без транзакции.

  • Гибридным подходом — частичное вложение «горячих» данных (например, имя и аватар автора поста), а остальное — по ссылке. Это баланс между производительностью и гибкостью.

Рекомендации по выбору:

  • Вкладывайте, если связь «один-ко-многим» и дочерние объекты всегда запрашиваются вместе с родителем (комментарии под постом, пункты заказа).
  • Ссылайтесь, если связь «многие-ко-многим» или дочерние объекты используются автономно (пользователи и роли, товары и категории).
  • Используйте частичное вложение для denormalization часто читаемых, редко изменяемых атрибутов (например, authorName в посте, даже если полный профиль — отдельный документ).

MongoDB также поддерживает валидацию схемы на уровне коллекции через JSON Schema. Это позволяет внедрить частичную строгость: например, требовать наличие поля email, проверять его формат регулярным выражением, задавать enum для status. Валидация применяется при вставке и обновлении, но не мешает добавлять опциональные расширения — гибкость сохраняется.


Антипаттерны и типичные ошибки

  • Хранение больших бинарных объектов в BinData — файлы (изображения, видео) размером более нескольких мегабайт следует хранить в объектных хранилищах (S3, MinIO), а в MongoDB оставлять только метаданные и URL. Для файлов до 16 МБ можно использовать GridFS — специализированный механизм, разбивающий файл на чанки.

  • Чрезмерная вложенность — вложенные объекты глубже 3–4 уровней усложняют запросы (field.subfield.subsubfield.value), затрудняют индексацию и увеличивают риск превышения лимита в 16 МБ. При необходимости — плоская структура с составными ключами.

  • Отсутствие TTL для временных данных — логи, сессии, кэши должны автоматически удаляться. Без TTL-индекса накопление данных приведёт к росту дискового пространства и замедлению операций.

  • Использование skip для глубокой пагинацииskip(100000) требует сканирования 100 000 документов. Альтернатива: курсорная пагинация по индексируемому полю (например, createdAt), где клиент передаёт последнее значение как маркер.

  • Создание индексов «на всякий случай» — каждый индекс замедляет запись. Регулярно анализируйте использование индексов через indexStats и удаляйте неиспользуемые.

  • Попытка имитировать JOIN’ы через вложенные $lookup в каждом запросе — если связанные данные нужны постоянно, лучше денормализовать. $lookup — для редких отчётов, а не для hot-path.